<template>
  <div class="safe-scan">
    <div class="scan-result-box" v-if="JSON.stringify(scanResultFields) !== '{}'">
      <el-tabs v-model="activeScanResult" @tab-click="handleChooseScanField">
        <el-tab-pane :label="resultItem.field_name" v-for="(resultItem, resultKey) in scanResultFields" :name="resultKey" :key="resultKey"></el-tab-pane>
      </el-tabs>
      <div class="scan-result-title">
        <el-button :type="scanResultType === '0' ? 'primary' : ''" size="small" round @click="chooseScanType('0')">全部</el-button>
        <el-button :type="scanResultType === key ? 'primary' : ''" v-for="(val, key) in scanResult" :key="key" size="small" round @click="chooseScanType(key)">
          {{ key === 'errorWords' ? '疑错词语' : key === 'riskWords' ? '风险词语' : '暗链/外链' }}·{{ val.length }}
        </el-button>
      </div>
      <div class="result-box-body-outer">
        <el-scrollbar>
          <div class="result-box-body" v-if="scanResultTable.length !== 0">
            <div
              class="result-word"
              v-for="(item, index) in scanResultTable"
              :key="index"
              :class="{
                'is-focus': focusId === item.id || (focusId == 'link' + item.id && item.type === 5),
                'level-1': item.levelId === 1,
                'level-2': item.levelId === 2,
                'level-3': item.levelId === 3,
                'level-link': !item.levelId
              }"
              @click="handleFocusWord(item)"
            >
              <div v-if="item.storeId">
                <div class="word-top">
                  <p @click.stop="handleChangeShowMore(item)">
                    <i class="iconfont" :class="{ 'is-collapse': item.showMore }">&#xe601;</i>
                    <span class="level-span">{{ item.level }}</span>
                    <span>{{ item.word }}</span>
                  </p>
                  <span class="adjust-word" v-show="item.adjustWord">建议:{{ item.adjustWord }}</span>
                </div>
                <el-collapse-transition>
                  <div class="word-more-msg" v-show="item.showMore">
                    <el-descriptions title="" :column="1">
                      <el-descriptions-item label="涉及法规:">
                        <span class="descriptions-text">{{ item.law }}</span>
                      </el-descriptions-item>
                      <el-descriptions-item label="描述:">
                        <span class="descriptions-text">{{ item.description }}</span>
                      </el-descriptions-item>
                    </el-descriptions>
                  </div>
                </el-collapse-transition>
                <div class="word-bottom">
                  <p>所在词库:{{ item.storeName }}</p>
                  <div class="button-box">
                    <el-button v-if="!readonly && isEditorClass(activeScanResult)" type="primary" round size="small" @click.stop="handleChangeWord(item)">替换</el-button>
                  </div>
                </div>
              </div>
              <div v-else>
                <div class="url-name" style="margin-bottom: 15px">
                  <span class="level-span">{{ item.level }}</span>
                  <el-tooltip :content="item.word">
                    <p>
                      <a :href="item.word" target="_blank">{{ item.word }}</a>
                    </p>
                  </el-tooltip>
                </div>
                <div class="word-bottom">
                  <p>外部链接</p>
                  <div class="button-box">
                    <el-button v-if="!readonly" type="primary" round size="small" @click="handleRemoveLink(item)">移除链接</el-button>
                    <el-button v-if="!readonly" type="primary" round size="small" @click="handleEditLink(item)">修改链接</el-button>
                  </div>
                </div>
              </div>
            </div>
          </div>
          <div class="result-box-body-empty" v-else>当前筛选下已经全部处理完成！</div>
        </el-scrollbar>
      </div>
      <div class="scan-result-footer">
        <slot name="footer"></slot>
        <el-button type="primary" @click="handleSafeScan">重新扫描</el-button>
      </div>
    </div>
    <div v-else class="no-scan-result">
      <i class="iconfont">&#xe642;</i>
      <p>暂无检测结果</p>
      <el-button type="primary" @click="handleSafeScan">去检测</el-button>
    </div>
    <!-- 检测动画弹窗 -->
    <el-dialog v-model="showScanning" title="安全检测" width="480px" :show-close="false" :align-center="true" :close-on-press-escape="false" :close-on-click-modal="false" append-to-body>
      <div class="scanning-box">
        <div class="scan-logo" v-loading="true" v-show="scanState === 1 || scanState === 3"></div>
        <div class="scan-logo" v-show="scanState === 2">
          <div class="success-logo">
            <i class="iconfont">&#xe693;</i>
          </div>
        </div>
        <p v-show="scanState === 1">安全检测中</p>
        <p v-show="scanState === 2">未检测到风险信息</p>
        <p v-show="scanState === 3">当前网络不稳定，请尝试取消并重新提交检测</p>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button v-if="scanState === 3" @click="handleCancelScan">取消</el-button>
          <el-button v-else @click="handleCloseScan" :disabled="scanState === 1">确认</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- 安全扫描结果确认 -->
    <el-dialog v-model="showScanResult" width="480px" title="检测结果" :align-center="true" append-to-body>
      <div class="result-confirm">
        <div>
          <p style="margin-bottom: 10px">
            检测成功,共
            <span style="color: red">{{ resultInfo.length }}</span>
            个字段异常
          </p>
          <p style="margin-bottom: 10px" v-for="(item, index) in resultInfo" :key="index">
            “{{ item.field }}”字段,外链
            <span class="result-link">{{ item.backLinksNum }}</span>
            条,疑错词汇
            <span class="result-error">{{ item.errorWordsNum }}</span>
            个,风险词汇
            <span class="result-risk">{{ item.riskWordsNum }}</span>
            个
          </p>
        </div>
      </div>
      <template v-if="!readonly" #footer>
        <span class="dialog-footer">
          <el-button size="small" v-show="showIgnore" @click="handleContinue">忽略并继续</el-button>
          <el-button type="primary" @click="handleResult">去处理</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
/* eslint-disable */
import { ref, watch, computed, reactive } from 'vue'
import { createBatchContentScanTask } from '@/api/safe'

const props = defineProps({
  // 检测表单
  fieldsValue: {
    type: Object,
    default: () => {}
  },
  // 表单字段映射
  fieldsMapping: {
    type: Object,
    default: () => {}
  },
  // 表单字段展开
  fieldsFlat: {
    type: Object,
    default: () => {}
  },
  // 编辑器实例
  editor: {
    type: Object,
    default: null
  },
  // 是否只读
  readonly: {
    type: Boolean,
    default: false
  }
})
const emits = defineEmits(['handleDeleteMark', 'handleSetSafeDrawer'])
const fieldsValue = computed(() => props.fieldsValue)
const tinymce = computed(() => props.editor)

const readonly = computed(() => props.readonly) // 是否只读,主要是因为审核模式下的安全扫描并没有使用tinymce编辑器
const showResult = ref(true)
const showIgnore = ref(false)
const scanState = ref(1) // 扫描状态(1: 扫描中, 2: 通过, 3: 网络问题)
const showScanning = ref(false) // 安全检测过程弹窗
const showScanResult = ref(false) // 扫描结果弹窗
const scanResultFields = ref({}) // 扫描结果字段
const resultInfo = ref([]) // 扫描结果总结
const activeScanResult = ref('') // 激活的字段扫描结果
const scanResultType = ref('0') // 字段的扫描结果类型（全部，风险词语，疑错词语，暗链）
// 当前字段的所有类型
const scanResult = computed(() => {
  return scanResultFields.value[activeScanResult.value].result
})
// 当前类型的所有违规词语
const scanResultTable = computed(() => {
  let resultList = []
  if (scanResultType.value === "0") {
    for (let item in scanResultFields.value[activeScanResult.value].result) {
      resultList = resultList.concat(scanResultFields.value[activeScanResult.value].result[item] || [])
    }
  } else {
    resultList = scanResultFields.value[activeScanResult.value].result[scanResultType.value]
  }
  return resultList
})
// 切换字段结果查看
const handleChooseScanField = (key) => {
  scanResultType.value = "0"
  focusId.value = 0
  activeScanResult.value = key.paneName
  handleFocusWord(scanResultTable.value[0])
}
// 安全扫描
const defaultOriginResult = reactive({
  isPass: false,
  resultMap: {}
})
const originResult = ref(Object.assign({}, defaultOriginResult)) // 父组件可能会基于接口返回结果再做处理，所以暴露
const handleSafeScan = async (ignoreFlag=false, resultFlag=true) => {
  originResult.value = Object.assign({}, defaultOriginResult)
  showIgnore.value = ignoreFlag // 默认情况下不展示忽略按钮，根据父组件调用情况决定
  showResult.value = resultFlag // 默认情况下不论扫描结果如何均展示结果弹窗，发布情况下如果通过则不展示
  scanState.value = 1
  showScanning.value = true
  emits('handleDeleteMark')
  try {
    // 删除无需检测的字段
    const fieldsValueCopy = structuredClone(fieldsValue.value)
    props.fieldsFlat.forEach(item => {
      if(!item.isScan){
        delete fieldsValueCopy[item.field]
      }
    })
    // 遍历 fieldsValueCopy 的属性，转换引用型数据为 JSON 字符串
    Object.keys(fieldsValueCopy).forEach(key => {
      const value = fieldsValueCopy[key]
      if (typeof value === 'object' && value !== null) {
        fieldsValueCopy[key] = JSON.stringify(value)
      }
    })
    const res = await createBatchContentScanTask({ contentMap: fieldsValueCopy, storeIds: null })
    originResult.value = Object.assign({}, res.data)
    scanResultFields.value = {}
    if (res.data.isPass) {
      scanState.value = 2
      if (!showResult.value) {
        showScanning.value = false
      }
    } else {
      resultInfo.value = []
      // 当前字段
      activeScanResult.value = Object.keys(res.data.resultMap)[0]
      // 当前类型
      scanResultType.value = '0'
      // 处理res数据
      for (let key in res.data.resultMap) {
        const fieldResult = res.data.resultMap[key]
        let _result = {
          field: props.fieldsMapping[key],
          backLinksNum: fieldResult.backLinks.length,
          errorWordsNum: fieldResult.errorWords.length,
          riskWordsNum: fieldResult.riskWords.length
        }
        resultInfo.value.push(_result)
        scanResultFields.value[key] = {
          field_name: props.fieldsMapping[key],
          result: fieldResult
        }
      }
      showScanning.value = false
      showScanResult.value = true
    }
  } catch {
    originResult.value.isPass = false
    scanState.value = 3
  }
  return originResult.value
}

// 取消扫描
const handleCancelScan = () => {
  showScanning.value = false
}
// 检测未通过时忽略结果
const ignoreMessage = ref('')
const handleContinue = () => {
  ElMessageBox.prompt("请填写忽略结果原因", "忽略原因", {
    confirmButtonText: "确认",
    inputValue: ignoreMessage.value,
    inputPattern: /\S/,
    inputErrorMessage: "原因不能为空",
  }).then(({ value }) => {
    fieldsValue.value.ignore_reason = value
    showScanResult.value = false
    emits('handleDeleteMark')
    emits('handleIgnore')
  })
}
// 关闭扫描
const handleCloseScan = () => {
  showScanning.value = false
}
// 处理结果
const handleResult = () => {
  showScanResult.value = false
  emits('handleSetSafeDrawer')
  // 聚焦focusID,如果当前字段是编辑器字段则定位词语,否则0
  if (isEditorClass(activeScanResult.value)) {
    handleFocusWord(scanResultTable.value[0])
  } else {
    handleFocusWord({ id: 0 })
  }
  for (const key in scanResultFields.value) {
    if (isEditorClass(key)) {
      // 标记文字（编辑器类,风险易错词汇）
      handleMarkKeyWords(key, [...scanResultFields.value[key].result.errorWords, ...scanResultFields.value[key].result.riskWords])
      // 标记链接（外链）
      handleMarkLinks(key, [...scanResultFields.value[key].result.backLinks])
    }
  }
}
// 标记文字
const handleMarkKeyWords = (editor, list) => {
  // 根据具体字段标记
  list.forEach((item) => {
    tinymce.value.editors[editor].setContent(join(tinymce.value.editors[editor].getContent(), item.word, item.levelId, item.id))
  })
}
// 标记链接
const handleMarkLinks = (editor, list) => {
  let $ = tinymce.value.editors[editor].dom.$
  list.forEach((item) => {
    let aLink = $('a[href="' + item.word + '"]')
    if (aLink.length > 0) {
      aLink.each(function () {
        $(this)
          .addClass("scan-words")
          .addClass("links-span")
          .attr("data-id", "link" + item.id)
          .attr("data-foxword", "yes")
      })
    } else {
      console.log("nononononon", item, aLink)
    }
  })
}
// 关键词标红
const join = (str, key, type, word) => {
  var reg = new RegExp(`(${key})`, "gi")
  let replace
  // data-foxWord属性标识扫描出的词汇
  switch (type) {
    case 1:
      replace = `<span class="scan-words violate-span" data-id="${word}" data-foxword="yes">$1</span>`
      break
    case 2:
      replace = `<span class="scan-words sensitivity-span" data-id="${word}" data-foxword="yes">$1</span>`
      break
  }
  if (replace) {
    return str.replace(reg, replace)
  } else {
    return str
  }
}
// 判断是否是编辑器字段
const isEditorClass = (field) => {
  const editorClass = props.fieldsFlat.filter((item) => item.isScan && item.component === 'editor').map((item) => item.field)
  return editorClass.includes(field)
}
// 选中词语
const focusId = ref(0)
const handleFocusWord = (data) => {
  if(readonly.value) return
  if (data && data.id) {
    if (data.type === 5) {
      focusId.value = "link" + data.id
    } else {
      focusId.value = data.id
    }
  }
}
// 监听焦点变化，在编辑器内高亮选中的词语
watch(
  () => focusId.value,
  (newVal) => {
    if (isEditorClass(activeScanResult.value)) {
      goAnchor(`#${activeScanResult.value} .scan-words[data-id="${newVal}"]`)
      if(props.editor){
        const $ = tinymce.value.editors[activeScanResult.value].dom.$
        $(".scan-words[data-id]").each(function () {
          $(this).removeClass("checked")
        })
        $(".scan-words[data-id=" + newVal + "]").each(function () {
          $(this).addClass("checked")
        })
      }
    }
  }
)
// 导航锚点
const goAnchor = (selector) => {
  if (document.querySelector(selector)) {
    document.querySelector(selector).scrollIntoView({
      behavior: "smooth",
    })
  }
}
// 展示词语违规详情
const handleChangeShowMore = (data) => {
  data.showMore = !data.showMore
}
// 切换扫描结果类型
const chooseScanType = (key) => {
  scanResultType.value = key
  handleFocusWord(scanResultTable.value[0])
}
// 替换词
const changeWord = ref("")
const handleChangeWord = (data) => {
  let $ = tinymce.value.editors[activeScanResult.value].dom.$
  if ($("span[data-id=" + data.id + "]").length === 0) {
    ElMessage({
      showClose: true,
      message: "该词语在内容中已不存在",
      type: "error",
      duration: 5 * 1000,
    })
    return
  }
  if (data.adjustWord) {
    changeWord.value = data.adjustWord
  } else {
    changeWord.value = ""
  }
  ElMessageBox.prompt("请填写需要替换的内容", "替换词语", {
    confirmButtonText: "确认",
    inputValue: changeWord.value,
  }).then(({ value }) => {
    handleWord(data)
    $("span[data-id=" + data.id + "]").each(function () {
      $(this).text(value)
      $(this).removeClass("scan-words").removeClass("checked").addClass("scan-words-change")
    })
    fieldsValue.value[activeScanResult.value] = tinymce.value.editors[activeScanResult.value].getContent()
  })
}
const handleRemoveLink = (data) => {
  let $ = tinymce.value.editors[activeScanResult.value].dom.$
  if ($(".scan-words[data-id=" + "link" + data.id + "]").length === 0) {
    ElMessage({
      showClose: true,
      message: "该在内容中已不存在",
      type: "error",
      duration: 5 * 1000,
    })
    return
  }
  handleWord(data)
  $(".scan-words[data-id=" + "link" + data.id + "]").each(function () {
    $(this).replaceWith($(this).html())
  })

  fieldsValue.value[activeScanResult.value] = tinymce.value.editors[activeScanResult.value].getContent()
}
const handleEditLink = (data) => {
  let tinyDom = tinymce.value.editors[activeScanResult.value].dom
  if (tinyDom.select(".scan-words[data-id=" + "link" + data.id + "]").length === 0) {
    ElMessage({
      showClose: true,
      message: "该在内容中已不存在",
      type: "error",
      duration: 5 * 1000,
    })
    return
  }
  ElMessageBox.prompt("链接地址", "修改链接", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    inputValue: data.word,
    inputPattern: /^(?:(http|https|ftp):\/\/)?((|[\w-]+\.)+[a-z0-9]+)(?:(\/[^/?#]+)*)?(\?[^#]+)?(#.+)?$/i,
    inputErrorMessage: "请输入正确的链接",
  }).then(({ value }) => {
    data.word = value
    tinyDom.setAttrib(tinyDom.select(".scan-words[data-id=" + "link" + data.id + "]"), "href", value)
  })
}
// 词语操作
const handleWord = (data) => {
  if (data.type === 1) {
    for (let i = 0; i < scanResultFields.value[activeScanResult.value].result.errorWords.length; i++) {
      if (scanResultFields.value[activeScanResult.value].result.errorWords[i].id === data.id) {
        // 删除对象的指定字段
        scanResultFields.value[activeScanResult.value].result.errorWords.splice(i, 1)
        break // 找到对象后可以立即跳出循环，提高效率
      }
    }
  } else if (data.type === 5) {
    for (let i = 0; i < scanResultFields.value[activeScanResult.value].result.backLinks.length; i++) {
      if (scanResultFields.value[activeScanResult.value].result.backLinks[i].id === data.id) {
        // 修改对象的指定字段
        scanResultFields.value[activeScanResult.value].result.backLinks.splice(i, 1)
        break // 找到对象后可以立即跳出循环，提高效率
      }
    }
  } else {
    for (let i = 0; i < scanResultFields.value[activeScanResult.value].result.riskWords.length; i++) {
      if (scanResultFields.value[activeScanResult.value].result.riskWords[i].id === data.id) {
        // 修改对象的指定字段
        scanResultFields.value[activeScanResult.value].result.riskWords.splice(i, 1)
        break // 找到对象后可以立即跳出循环，提高效率
      }
    }
  }
}

defineExpose({
  handleSafeScan,
  originResult
})
</script>

<style lang="scss" scoped>
.safe-scan {
  .scan-result-box {
    ::v-deep(.el-tabs__item) {
      height: 30px !important;
      line-height: 30px !important;
    }
    .result-box-body-outer {
      height: calc(100vh - 390px);
    }
    .scan-result-footer {
      display: flex;
      justify-content: flex-end;
      margin: 10px 0;
    }
    .result-box-body {
      .result-word.level-1 {
        border-color: #fd463e;
        &.is-focus {
          background-color: rgba(253, 70, 62, 0.1);
        }
        .word-top p span {
          color: #fd463e;
        }
        .word-top p span.level-span {
          background-color: #fd463e;
        }
      }
      .result-word.level-2 {
        border-color: #ff9700;
        &.is-focus {
          background-color: rgba(255, 151, 0, 0.1);
        }
        .word-top p span {
          color: #ff9700;
        }
        .word-top p span.level-span {
          background-color: #ff9700;
        }
      }
      .result-word.level-link {
        border-color: #4d99f2;
        &.is-focus {
          background-color: rgba(#4d99f2, 0.1);
        }
        .url-name {
          display: flex;
          p {
            @include textEllipsis(18px, 1);
            a {
              color: #4d99f2;
            }
          }
          span.level-span {
            flex: none;
            display: block;
            height: 20px;
            line-height: 20px;
            background-color: #4d99f2;
            color: #fff;
            padding: 0 5px;
            border-radius: 4px;
            margin-right: 10px;
            font-size: 14px;
          }
        }
        .word-top p span.level-span {
          background-color: #4d99f2;
        }
      }
      .result-word {
        width: 100%;
        border: 1px solid #fd463e;
        border-top-width: 4px;
        margin-top: 10px;
        box-sizing: border-box;
        padding: 10px 10px;
        .word-top {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-bottom: 15px;
          p {
            cursor: pointer;
            display: flex;
            align-items: center;
          }
          p i {
            transition: all 0.3s;
            transform: rotate(-90deg);
            &.is-collapse {
              transform: rotate(0);
            }
          }
          p span {
            color: #fd463e;
          }
          p span.level-span {
            display: block;
            height: 20px;
            line-height: 20px;
            background-color: #fd463e;
            color: #fff;
            padding: 0 5px;
            border-radius: 4px;
            margin: 0 10px;
            font-size: 14px;
          }
          span.adjust-word {
            color: #17b336;
          }
        }
        .word-more-msg {
          ::v-deep(.el-descriptions__body) {
            background-color: inherit;
          }
        }
        .word-bottom {
          display: flex;
          justify-content: space-between;
          align-items: center;
          p {
            font-size: 14px;
          }
        }
      }
      .no-data {
        height: 300px;
        width: 100%;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        i {
          font-size: 80px;
        }
      }
    }
    .result-box-body-empty {
      margin-top: 30px;
    }
  }
  .no-scan-result {
    width: 100%;
    height: 100%;
    margin-top: 30px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    i {
      font-size: 80px;
      color: #aaaaaa;
    }
    p {
      margin-bottom: 20px;
      color: #aaaaaa;
    }
  }
}
.scanning-box {
  display: flex;
  align-items: center;
  flex-direction: column;
  justify-content: center;
  .scan-logo {
    height: 60px;
    width: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-bottom: 15px;
    .success-logo {
      height: 60px;
      width: 60px;
      border-radius: 50%;
      background-color: #2cbf77;
      display: flex;
      align-items: center;
      justify-content: center;
      i {
        font-size: 43px;
        color: #fff;
      }
    }
  }
}
</style>
